<?php

/* Tecler - Simple PHP class for developing tag-based parser engines
 * Version 0.9.0
 * Copyright 2009, Virthos Systems GmbH
 * http://code.google.com/tecler
 * ===================================================================
 *
 * DESCRIPTION
 *
 * Tecler is a simple, easy-to-use class for developing tag-based
 * parsers, compilers and interpreters. You may define your own macro
 * language and let Tecler compile or interprete templates based on this
 * language. Tecler may also be used as an lightweight alternative to
 * full-blown XML parsers when XML structures are not too complex.
 *
 * The parser functionality is defined through library classes that
 * provide functions whose names correspond to tag names. They may
 * also provide hook functions that are called when specific events
 * occur (begin, end, no tag function found etc.) 
 *
 *
 * USAGE
 *
 * // Example 1
 * // The library class exampleLib is defined in example_lib.php and
 * // extends the tecler class. So there is no need for registering
 * // a class object.
 * require_once( 'example_lib.php' );
 * $lib = new exampleLib;
 * $lib->parseString( $some_code );
 *
 * // Example 2
 * // Here are two library classes, none of them extends the tecler
 * // class. So we have to create an instance of the tecler class and
 * // register the library classes in it.
 * require_once( 'example_lib1.php' );
 * require_once( 'example_lib2.php' );
 * $lib1 = new exampleLib1;
 * $lib2 = new exampleLib2;
 * $t = new tecler;
 * $t->registerLibrary( $lib1 );
 * $t->registerLibrary( $lib2 );
 * $t->parseString( $some_code );
 * 
 * 
 * 
 * HOOK FUNCTIONS
 * 
 * This is a list of supported hook functions:
 * 
 * _begin()
 * Is called once at the beginning of the parse process.
 * 
 * _end()
 * Is called once at the end of the parse process.
 * 
 * _cdata($code)
 * Is called for every chunk of code which is not a tag.
 * 
 * _default($name [,$args])  
 * Is called when there is no tag function with name $name. 
 * 
 * _getname($name)
 * Is called before trying to call a tag function. Can be used to modify
 * the name under which Tecler looks for the tag function. 
 * 
 * If there are multiple library classes defining the same function 
 * Tecler calls the one that has been registered first. If no class
 * defines a particular function, Tecler tries to call a method of that
 * name for the current object ("$this"). If this does not exists either
 * the _default hook function is called.
 * 
 * 
 *
 *
 * TODO
 * - Modify compileFile method in order to handle larger files (chunks)
 * - Implement some kind of caching
 *
 *
 * LICENSE
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of the
 * License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 *
 */


/**
 * Class tecler.
 * To use, create an instance of the class. Then register some class 
 * objects using registerLibrary(). After that call parseString() 
 * or parseFile() to invoke the actual parsing process.
 */
class tecler {
	
	 /**
	  * Field tagStart.
	  * The character or string defining the beginning of a tag.
	  * Defaults to '<'.
	  */
	 var $tagStart = '<';

	 /**
	  * Field tagEnd.
	  * The character or string defining the end of a tag.
	  * Defaults to '>'.
	  */
	 var $tagEnd = '>';

	 /**
	  * Field tagSplit.
	  * The character or string which separates the tag attributes
	  * from the tag name.
	  * Defaults to ' ' (single space).
	  */
	 var $tagSplit = ' ';

	 /**
	  * Field passParameterArray.
	  * If true, Tecler passes parameters to tag functions as an array
	  * of key/value pairs. This makes sense only if parameter strings
	  * are formatted in an HTML/XML-like syntax, i.e. 
	  * param1="value1" param2="value2" etc. The first element of the
	  * array is always the raw parameter string itself.
	  * Defaults to false.
	  */
	 var $passParameterArray = false;

	 /**
	  * Field debugMode.
	  * If true Tecler outputs verbose debug messages to $debugOutput.
	  * Defaults to false.
	  */
	 var $debugMode = false;

	 /**
	  * Field debugOutput.
	  * Gets filled with debug messages if debugMode is set to true.
	  */
	 var $debugOutput;


	 /**
	  * Field libClasses.
	  * Array of class objects which should be consulted in order to
	  * to find a processing function for a particular tag.
	  * Defaults to empty array.
	  */
	 var $libClasses;


	/*** PUBLIC METHODS ***/
	

	/**
	 * Method registerLibrary.
	 * Adds a class object to the classes array.
	 */
	function registerLibrary( $classObject ){
		$this->log( 'registerLibrary: ' . get_class( $classObject ) );
		$this->libClasses[] =& $classObject ;
	}
	

	/**
	 * Method parseString.
	 * Parses a string.
	 */
	function parseString( $string ){
		$this->log( 'parseString: ' . strlen( $string ) . ' Byte(s)' . ( strlen($string) < 100 ? ': ' . $string : '' ) );
		return $this->parse( $string );
	}


	/**
	 * Method parseFile.
	 * Parses a text file.
	 */
	function parseFile( $filePath ){
		$this->log( 'parseFile: ' . $filePath );
		if( file_exists( $filePath ) ) {
			$string = file_get_contents( $filePath );
			return $this->parse( $string );
		}
	}


	/**
	 * Method parseParameters.
    * Parses a parameter string and returns an array with key/value 
    * pairs. The string must be in an XML/HTML-like format: 
    * Var1="Value1" Var2="Value2" etc.
    * Values may be surrounded by single or double quotes, but
    * pairs must match. Whitespace before and after the equal signs 
    * is ignored.	Vars may be separated by comma.Stand-alone vars 
    * (without equal sign and value) are returned as empty strings.
	 */
	function parseParameters( $parameterString ){
 
		$str = preg_replace ( '/(=\s*)\'([^\']*)\'/', '$1"$2"', $parameterString ); // normalize quotes
		$reg = preg_split( '/(\S+\s*=\s*"[^"]*")/', $str, -1, PREG_SPLIT_DELIM_CAPTURE );

		$result = array();

		foreach( $reg as $item ){
			if( strpos( $item, '=' ) > 0 ){
				$arr = explode( '=', $item );
				$result[ trim($arr[0]) ] = substr( trim($arr[1]), 1, -1 );
			} else {
				$keys = preg_split( '/[\s,]+/', $item );
				foreach( $keys as $key ){
					if( $key ){
						$result[$key] = '';
					}
				}
			}
		}
		return $result;
	}


	/*** PRIVATE METHODS ***/

	function parse( $code ){
		$str = $this->handleFunctionCall('_begin');
		$arr = explode( $this->tagStart, $code );
		$str .= $this->handleCData( $arr[0] );

		for( $i = 1; $i < sizeof( $arr ); $i++ ){
			$pos = strpos( $arr[$i], $this->tagEnd );
			$str .= $this->handleTag( substr( $arr[$i], 0, $pos) );
			$str .= $this->handleCData( substr( $arr[$i], $pos + strlen($this->tagEnd) ) );
		}

		$str .= $this->handleFunctionCall('_end');
		return $str;
	}


	function handleCData( $code ){

		if( $this->existsFunction('_cdata' ) ){
			return $this->handleFunctionCall( '_cdata', $code );
		}

		return $code;
	}
	

	function handleTag( $code ){

		$pos = strpos( $code, $this->tagSplit ); 
		
		if( $pos ){
			$tag = strtolower( trim( substr( $code, 0, $pos ) ) );
			$attribs = trim( substr( $code, $pos + 1 ) );
		} else {
			$tag = strtolower( trim( $code ) );
			$attribs = '';
		}
		
		if( $this->existsFunction('_getname') ){
			$tag = $this->handleFunctionCall( '_getname', $tag );
		}

		if( $this->passParameterArray ){
			$arr = $this->parseParam( $attribs );
			array_unshift( $arr, $attribs );
			$attribs = $arr;
		}

		$processed = false;
		
		if( substr( $tag, 0, 1 ) != '_' ){
			if( $this->existsFunction( $tag ) ){
				$str = $this->handleFunctionCall( $tag, $attribs );
				$processed = true;
			}
		} 
		
		if( ! $processed ) {
			$originalString = $this->tagStart . $code . $this->tagEnd;
			if( $this->existsFunction('_default') ){
				$str = $this->handleFunctionCall( '_default', array( $tag, $attribs ) );
			} else {
				$this->log( 'handleTag: ' . $tag . '(' . $attribs . ') -> no handler found');
				$str = $originalString;
			}
		}

		return $str;
	}


	function existsFunction( $name ){

		if( $name == '' ){
			return false;
		}
		
		$name = strtolower( $name );

		for( $i = 0; $i < count($this->libClasses); $i++ ){
			if( method_exists( $this->libClasses[$i], $name ) ){
				return true;
			}
		}
		
		if( method_exists( $this, $name ) ){
			return true;
		}
		
		return false;
	}


	function handleFunctionCall( $name, $params = '' ){

		if( $name == '' ){
			return '';
		}

		$name = strtolower( $name );

		for( $i = 0; $i < count($this->libClasses); $i++ ){
			if( method_exists( $this->libClasses[$i], $name ) ){
				$this->log( 'handleFunctionCall: ' . $name . '(' . ( is_array($params) ? $params[0].','.$params[1] : $params ) . ') -> handled by ' . get_class( $this->libClasses[$i] ) );
				return $this->libClasses[$i]->$name( $params );
			}
		}

		if( method_exists( $this, $name ) ){
			$this->log( 'handleFunctionCall: ' . $name . '(' . ( is_array($params) ? $params[0].','.$params[1] : $params ) . ') -> handled by ' . get_class( $this ) );
			return $this->$name( $params );
		}

		$this->log( 'handleFunctionCall: ' . $name . '(' . $params . ') -> no handler found');

		return false;
	}
	

	function log( $message ){
		if( $this->debugMode ){
			$this->debugOutput .= "\n" . $message;
		}
	}
}

?>